package de.dailab.graphworld.bean;

import de.dailab.graphworld.ontology.Edge;
import de.dailab.graphworld.ontology.Node;
import de.dailab.graphworld.ontology.Task;
import de.dailab.graphworld.ontology.exceptions.CapacityExceededException;
import de.dailab.graphworld.ontology.exceptions.ContestEndedException;
import de.dailab.graphworld.ontology.exceptions.ContestNotStartedException;
import de.dailab.graphworld.ontology.exceptions.GraphWorldException;
import de.dailab.graphworld.ontology.exceptions.InvalidPositionException;
import de.dailab.graphworld.ontology.exceptions.MoveException;
import de.dailab.graphworld.ontology.exceptions.MoveIntervalException;
import de.dailab.graphworld.ontology.exceptions.NoGoldCarriedException;
import de.dailab.graphworld.world.ExtendedGraphWorldAgentBean;
import de.dailab.graphworld.world.PathfinderAgentBean;
import de.dailab.jiactng.agentcore.action.Action;
import de.dailab.jiactng.agentcore.action.ActionResult;
import de.dailab.jiactng.agentcore.ontology.IActionDescription;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * An abstract implementation of a {@link TeamMember} that occurs in the graph
 * world. Scouts and carriers are implementations of this class, broker not.
 *
 * @author Kseniya
 */
public abstract class BeanPhysical extends TeamMember {

    private IActionDescription actionInit = null;
    private IActionDescription actionFindPath = null;
    protected IActionDescription actionTakeGold;
    protected IActionDescription actionDropGold;
    protected IActionDescription actionMove;
    private IActionDescription actionExplore;
    protected Node storedPos = null;
    private Integer maxCapacity;
    private Integer goldAmountCarrying = 0;

    /**
     * Per default, the team member is not placed in world at the first
     * execute(...) call, so an appropriate action must be invoked to assign
     * this bean to a team.
     *
     * @return
     */
    @Override
    protected boolean isInitialised() {
        return super.isInitialised()
                && actionFindPath != null
                && storedPos != null
                && actionMove != null
                && actionExplore != null
                && actionTakeGold != null
                && actionDropGold != null;
    }

    @Override
    protected void initialise() {
        super.initialise();
        if (actionFindPath == null) {
            actionFindPath = thisAgent.searchAction(new Action(PathfinderAgentBean.ACTION_GETPATH));
        }
        if (actionExplore == null) {
            actionExplore = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_EXPLORE));
        }
        if (actionMove == null) {
            actionMove = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_MOVE));
        }
        if (actionTakeGold == null) {
            actionTakeGold = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD));
        }
        if (actionDropGold == null) {
            actionDropGold = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD));
        }
        if (actionInit == null) {
            actionInit = thisAgent.searchAction(new Action(ExtendedGraphWorldAgentBean.ACTION_INIT_TEAM_POSITION));
        }
        if (actionInit != null) {
            this.invoke(actionInit, new Serializable[]{getTeamName()}, this);
        }
    }

    /**
     * Invokes the GETPATH action.
     *
     * @param srcID where from
     * @param targetID where to
     */
    protected void invokeFindPath(int srcID, int targetID) {
        invoke(actionFindPath, new Serializable[]{new Node(srcID), new Node(targetID)});
    }

    protected void explore() {
        this.invoke(actionExplore, null, this);
    }

    protected void move(Node target) {
        this.invoke(actionMove, new Serializable[]{target}, this);
    }

    /**
     * should be called after successful explore only. Updates memory, selects
     * next node to move to and queues an appropriate move action.
     *
     * @param explored set of explored nodes
     */
    protected void consumeExplore(Set<Node> explored) {
        // do nothing by default
    }

    /**
     * Invokes the GETPATH action, waits for result and consumes it.
     *
     * @param srcID where from
     * @param targetID where to
     */
    protected List<Edge> findPath(int srcID, int targetID) {
        return (List<Edge>) invokeAndWaitForResult(actionFindPath, new Serializable[]{new Node(srcID), new Node(targetID)}).getResults()[0];
    }

    @Override
    public void receiveResult(ActionResult result) {
        String actionName = result.getAction().getName();
        Serializable failure = result.getFailure();
        if (failure == null) {
            switch (actionName) {
                case ExtendedGraphWorldAgentBean.ACTION_INIT_TEAM_POSITION:
                    // consume start position returned on initialisation call
                    storedPos = (Node) result.getResults()[0];
                    consumeInit();
                    break;
                case PathfinderAgentBean.ACTION_GETPATH:
                    consumePath((List<Edge>) result.getResults()[0]);
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_EXPLORE:
                    Set<Node> neighbours = new HashSet<>((Set<Node>) result.getResults()[0]);
                    consumeExplore(neighbours);
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_MOVE:
                    Node movedTo = (Node) result.getResults()[0];
                    consumeMove(movedTo);
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_TAKE_GOLD:
                    int goldTaken = ((Integer) result.getResults()[0]).intValue();
                    consumeTakeGold(goldTaken);
                    break;
                case ExtendedGraphWorldAgentBean.ACTION_DROP_GOLD:
                    consumeDropGold();
                    break;
                default:
                    break;
            }
        } else {
            if (failure instanceof GraphWorldException) {
                GraphWorldException handleable = (GraphWorldException) failure;
                handleFailure(actionName, handleable);
            } else {
                log.error(actionName);
                log.error(result.getFailure());
            }
        }
        super.receiveResult(result);
    }

    /**
     * Override this method if you are using path finding.
     *
     * @param path returned on {@link #invokeFindPath(int, int)} call.
     */
    protected void consumePath(List<Edge> path) {
        // do nothing per default
    }

    protected void consumeInit() {
        // do nothing per default
    }

    protected int getFreeCapacity() {
        return Math.max(0, getMaxCapacity() - getGoldAmountCarrying());
    }

    public Integer getMaxCapacity() {
        return maxCapacity;
    }

    public void setMaxCapacity(Integer maxCapacity) {
        this.maxCapacity = maxCapacity;
    }

    public Integer getGoldAmountCarrying() {
        return goldAmountCarrying;
    }

    public void setGoldAmountCarrying(Integer goldAmountCarrying) {
        this.goldAmountCarrying = goldAmountCarrying;
    }

    /**
     * Selects the task which gold node is the nearest to our position.
     *
     * @param tasksAvailable available tasks to select from
     * @param from node where we currently are
     * @return the nearest task
     */
    protected Task selectNearestTask(Collection<Task> tasksAvailable, Node from) {
        List<Task> tasks = new ArrayList<>(tasksAvailable);
        // calc paths to all tasks' nodes first
        Map<Task, List<Edge>> paths = new HashMap<>();

        for (Task task : tasks) {
            Node target = task.goldNode;
            List<Edge> path;
            if (from.getID() == target.getID()) {
                path = Collections.emptyList();
            } else {
                path = findPath(from.getID(), target.getID());
            }
            paths.put(task, path);
        }
        // now select task with the shortest path
        Task nearest = null;
        int minDist = Integer.MAX_VALUE;
        for (Map.Entry<Task, List<Edge>> entry : paths.entrySet()) {
            if (minDist > entry.getValue().size()) {
                minDist = entry.getValue().size();
                nearest = entry.getKey();
            }
        }
        return nearest;
    }

    protected void handleFailure(String actionName, GraphWorldException ex) {
//        log.error(this.getClass().getSimpleName() + ": error occured " + ex);
        Node pos = ex.getPosition();
        if (ex instanceof CapacityExceededException) {
            handleCapacityExceeded(pos);
        } else if (ex instanceof ContestEndedException) {
            handleContestEnded(actionName);
        } else if (ex instanceof ContestNotStartedException) {
            handleContestNotStarted(actionName);
        } else if (ex instanceof InvalidPositionException) {
            handleInvalidPosition(actionName, pos);
        } else if (ex instanceof MoveException) {
            handleMoveFailure(((MoveException) ex).getProblem(), pos);
        } else if (ex instanceof MoveIntervalException) {
            handleMoveIntervalFailure(pos);
        } else if (ex instanceof NoGoldCarriedException) {
            handleNoGoldCarriedFailure(pos);
        } else {
            log.error("Unhandled exception occured! " + ex);
        }
    }

    /**
     * Can be invoked after take gold action only!
     *
     * @param position
     */
    protected abstract void handleCapacityExceeded(Node position);

    protected abstract void handleContestEnded(String actionName);

    protected abstract void handleContestNotStarted(String actionName);

    /**
     * Can be invoked after either explore, move, take or drop gold action.
     *
     * @param actionName
     * @param position
     */
    protected abstract void handleInvalidPosition(String actionName, Node position);

    /**
     * Can be invoked after move action only!
     *
     * @param cause
     * @param position
     */
    protected abstract void handleMoveFailure(MoveException.Problem cause, Node position);

    /**
     * Can be invoked after move action only!
     *
     * @param position
     */
    protected abstract void handleMoveIntervalFailure(Node position);

    /**
     * Can be invoked after drop gold action only!
     *
     * @param position
     */
    protected abstract void handleNoGoldCarriedFailure(Node position);

    protected void consumeMove(Node movedTo) {
        storedPos = movedTo;
    }

    protected void consumeTakeGold(int goldTaken) {
        setGoldAmountCarrying(goldAmountCarrying + goldTaken);
    }

    protected void consumeDropGold() {
        // we do not carry any gold anymore
        setGoldAmountCarrying(0);
    }

    protected class ActionContainer {

        protected final IActionDescription action;
        protected final Serializable[] params;

        public ActionContainer(IActionDescription action, Serializable[] params) {
            this.action = action;
            this.params = params;
        }

        void perform() {
            Serializable[] actionParams;
            if (action == actionTakeGold) {
                // always have the free capacity up to date
                actionParams = new Serializable[]{Integer.valueOf(getFreeCapacity())};
            } else {
                actionParams = params;
            }
            BeanPhysical.this.invoke(action, actionParams, BeanPhysical.this);
        }

        @Override
        public String toString() {
            String params2Str = "";
            if (params != null && params.length > 0 && params[0] != null) {
                params2Str += ((Node) params[0]).getID();
            }
            return action.getName() + " " + params2Str;
        }
    }
}
